package com.iohao.game.action.skeleton.core.codec;

import cn.hutool.core.util.ReflectUtil;
import com.esotericsoftware.asm.ClassWriter;
import com.google.protobuf.GeneratedMessageV3;
import com.iohao.game.common.consts.CommonConst;
import com.iohao.game.common.kit.ProtoKit;
import org.springframework.asm.Opcodes;

import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;

public class ProtoV3DataCodec extends ClassLoader implements DataCodec {
    private static final ConcurrentMap<Class<?>, IDecoder> DECODER_CACHE = new ConcurrentHashMap<>();
    private static final AtomicLong CLASS_ID = new AtomicLong(0);

    @Override
    public byte[] encode(Object data) {
        if (data instanceof GeneratedMessageV3 message) {
            return message.toByteArray();
        }

        return ProtoKit.toBytes(data);
    }

    @Override
    public String codecName() {
        return "protobuf3";
    }

    @Override
    public <T> T decode(byte[] data, Class<?> dataClass) {
        Objects.requireNonNull(dataClass);

        if (Objects.isNull(data)) {
            data = CommonConst.emptyBytes;
        }

        if (!GeneratedMessageV3.class.isAssignableFrom(dataClass)) {
            //noinspection unchecked
            return (T) ProtoKit.parseProtoByte(data, dataClass);
        }

        IDecoder decoder = DECODER_CACHE.computeIfAbsent(dataClass, clazz -> {
            ClassWriter classWriter = new ClassWriter(0);

            String decoderClassName = "_" + CLASS_ID.incrementAndGet() + "/" + dataClass.getSimpleName() + "_Decoder";
            String dataClassName = dataClass.getName().replace('.', '/');

            classWriter.visit(Opcodes.V17, Opcodes.ACC_PUBLIC, decoderClassName,
                    null, "java/lang/Object", new String[]{IDecoder.class.getName().replace('.', '/')});

            {
                //     创建构造方法
                var methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
                methodVisitor.visitCode();
                methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
                methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
                methodVisitor.visitInsn(Opcodes.RETURN);
                methodVisitor.visitMaxs(1, 1);
            }

            {
                //     Object decode(byte[] data); 方法
                var methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "decode", "([B)Ljava/lang/Object;", null, new String[]{"java/lang/Exception"});
                // call parseFrom
                methodVisitor.visitCode();
                methodVisitor.visitVarInsn(Opcodes.ALOAD, 1);
                methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, dataClassName, "parseFrom", "([B)L" + dataClassName + ";", false);
                methodVisitor.visitInsn(Opcodes.ARETURN);
                methodVisitor.visitMaxs(1, 2);
                methodVisitor.visitEnd();
            }

            byte[] code = classWriter.toByteArray();
            // define class and create instance
            try {
                return (IDecoder) ReflectUtil.newInstance(this.defineClass(null, code, 0, code.length));
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });

        try {
            //noinspection unchecked
            return (T) decoder.decode(data);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
